package com.bbn.openmap.omGraphics.util;

import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.bbn.openmap.MoreMath;
import com.bbn.openmap.geo.Geo;
import com.bbn.openmap.geo.GeoArray;
import com.bbn.openmap.geo.Intersection;
import com.bbn.openmap.geo.Ribbon;
import com.bbn.openmap.geo.RibbonIterator;
import com.bbn.openmap.omGraphics.OMAreaList;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.omGraphics.OMPoly;
import com.bbn.openmap.proj.Length;

/**
 * The RibbonMaker class takes polygon coordinates and creates another polygon
 * from it, a buffer region based on ground distance, around the original poly.
 * It assumes that the polygon coordinates are going in a clockwise direction.
 * The returned polygon is actually an OMAreaList made up of OMPolys, OMArcs and
 * OMLines, depending on the shape of the original polygon.
 * <p>
 * 
 * The RibbonMaker is created from one of the factory methods that designates
 * whether the coordinates are radians or decimal degrees. FYI - OMPoly
 * coordinates retrieved from the getLatLonArray method are in radians. After
 * the RibbonMaker is created, call the method that creates the appropriate
 * shape. For now, getOuterRing is your only option. To use:
 * 
 * <pre>
 * 
 * llPoints = new double[] { 40.0f, -92.0f, 42.0f, -87.0f, 38.57, -90.825, 37.0f, -89.0f, 35.0f, -94.0f, 40.0f,
 * 		-92.0f };
 * 
 * OMGraphic omg = RibbonMaker.createFromDecimalDegrees(llPoints).getOuterRing(Length.MILE.toRadians(100));
 * 
 * </pre>
 * 
 * 
 * @author ddietrick
 */
public class RibbonMaker {

	protected GeoArray geoCoords;
	// If a negative distance is passed to getOuterRing, then we'll construct
	// this with opposite winding
	protected GeoArray geoCoords_reversed = null;
	private static final Logger logger = Logger.getLogger("com.bbn.openmap.omGraphics.util.RibbonMaker");
	protected final static int STRAIGHT = 0;
	protected final static int BENDS_LEFT = -1;
	protected final static int BENDS_RIGHT = 1;
	protected double bufferLimit = 4.778825E-10;

	protected RibbonMaker(GeoArray gCoords) {
		geoCoords = gCoords;
	}

	/**
	 * Create a RibbonMaker from decimal degree coordinates.
	 * 
	 * @param coords lat,lon,lat,lon in decimal degrees.
	 * @return RibbonMaker
	 */
	public static RibbonMaker createFromDecimalDegrees(double[] coords) {
		return new RibbonMaker(GeoArray.Double.createFromLatLonDegrees(coords));
	}

	/**
	 * Create a RibbonMaker from radian coordinates.
	 * 
	 * @param coords lat,lon,lat,lon in radians.
	 * @return RibbonMaker
	 */
	public static RibbonMaker createFromRadians(double[] coords) {
		return new RibbonMaker(GeoArray.Double.createFromLatLonRadians(coords));
	}

	/**
	 * Assumes coords represent a polygon, returns an OMAreaList representing
	 * buffer zone around the outside of a polygon.
	 * 
	 * @param dist distance of buffer area, in radians. Use Length to convert.
	 * @return OMAreaList of a polygon that is a distance away from the
	 *         coordinate polygon's edges.
	 */
	public OMAreaList getOuterRing(double dist) {
		OMAreaList ret = new OMAreaList();

		if (Math.abs(dist) <= bufferLimit) {
			return ret;
		}

		GeoArray localGeoCoords = this.geoCoords;
		if (dist < 0) {
			// Construct a version of this shape wound the other way
			dist = -dist;
			if (geoCoords_reversed == null) {
				double[] reversed = new double[2 * geoCoords.getSize()];
				for (int i = geoCoords.getSize() - 1; i >= 0; i--) {
					Geo thisGeo = geoCoords.get(geoCoords.getSize() - i - 1);
					reversed[2 * i] = thisGeo.getLatitudeRadians();
					reversed[2 * i + 1] = thisGeo.getLongitudeRadians();
				}
				geoCoords_reversed = GeoArray.Double.createFromLatLonRadians(reversed);
			}
			localGeoCoords = geoCoords_reversed;
		}

		int numCoords = localGeoCoords.getSize();

		if (numCoords >= 3) {

			Geo g1 = localGeoCoords.get(0);
			Geo g2 = localGeoCoords.get(1);
			Geo g3 = localGeoCoords.get(2);

			handlePointsForOuterRing(g1, g2, g3, dist, ret);

			for (int i = 3; i < numCoords; i++) {
				g1 = g2;
				g2 = g3;
				g3 = localGeoCoords.get(i);

				handlePointsForOuterRing(g1, g2, g3, dist, ret);
			}

			// test, and close it off if needed
			if (!localGeoCoords.get(0).equals(localGeoCoords.get(numCoords - 1))) {
				g1 = g2;
				g2 = g3;
				g3 = localGeoCoords.get(0);

				handlePointsForOuterRing(g1, g2, g3, dist, ret);
			}

			// Now round out the first and last segment, centering on the first
			// coordinate
			g1 = g2;
			g2 = g3;
			g3 = localGeoCoords.get(1);

			handlePointsForOuterRing(g1, g2, g3, dist, ret);

		}
		return ret;
	}

	/**
	 * Takes a corner represented by the three geos, and adds OMGraphics to the
	 * OMAreaList depending on which way the corner bends - for right turns,
	 * it'll add an OMLine, OMArc and OMLine. The OMLines will go from half the
	 * distance of the legs to the rounded corner. The left turn will have a
	 * polygon added.
	 * 
	 * @param g1 point 1
	 * @param g2 point 2
	 * @param g3 point 3
	 * @param dist buffer distance in radians
	 * @param ret OMAreaList to add OMGraphics to.
	 */
	protected void handlePointsForOuterRing(Geo g1, Geo g2, Geo g3, double dist, OMAreaList ret) {
		int bend = bends(g1, g2, g3);

		Geo gret = g3;
		RibbonIterator leg1 = new RibbonIterator(g1, g2, dist);
		OMPoly poly1 = getHalfPoly(leg1, Ribbon.LEFT, false);

		RibbonIterator leg2 = new RibbonIterator(g2, g3, dist);
		OMPoly poly2 = getHalfPoly(leg2, Ribbon.LEFT, true);

		// Oh dear...
		if (poly1 == null || poly2 == null) {
			return;
		}

		if (bend == STRAIGHT || g2.equals(g3)) {
			ret.add(poly1);
			ret.add(poly2);
		} else {
			if (bend == BENDS_LEFT) {
				// short, need to find intersection of two legs and remove
				// points
				// from polys to only go to intersection

				double dg12 = g1.distance(g2);
				double dg23 = g2.distance(g3);
				double legTestDist = dist * 2;

				if (dg12 < legTestDist || dg23 < legTestDist) {
					addShortLegPolyForIntersection(g1, g2, g3, Ribbon.LEFT, dist, ret);
				} else {
					addPolyForIntersection(poly1, poly2, dist, ret);
				}

			} else {
				OMGraphic omp = getPushbackPoly(poly1, dist);
				if (omp != null) {
					ret.add(omp);
				}
				// Add OMArc in the middle, rounding around a corner
				OMGraphic oma = getArc(g2, poly1, poly2);
				if (oma != null) {
					ret.add(oma);
				}
				omp = getPushbackPoly(poly2, dist);
				if (omp != null) {
					ret.add(omp);
				}
			}
		}

	}

	/**
	 * Method that determines which way the angle between the three points
	 * bends.
	 * 
	 * @param g1
	 * @param g2
	 * @param g3
	 * @return STRAIGHT if no bend, BENDS_LEFT if bends less than PI,
	 *         BENDS_RIGHT if bends more than PI.
	 */
	protected int bends(Geo g1, Geo g2, Geo g3) {
		double bend = g1.crossNormalize(g2).distance(g3) - (Math.PI / 2.0);

		if (Math.abs(bend) < .0001) {
			return STRAIGHT; // essentially straight
		} else {
			if (bend < 0) {
				return BENDS_LEFT;
			}
		}

		return BENDS_RIGHT;
	}

	/**
	 * Checks to see if a point is too close to any side of the original
	 * polygon.
	 * 
	 * @param pnt
	 * @param distance
	 * @return true if is too close and should not be added to the buffer
	 *         polygon.
	 */
	protected boolean tooClose(Geo pnt, double distance) {
		return Intersection.isPointNearPoly(pnt, geoCoords, distance - bufferLimit);
	}

	/**
	 * Takes a poly that's going to be added to the buffer and removes any
	 * points that may be too close to the original poly.
	 * 
	 * @param omp the buffer poly to be added later
	 * @param dist the distance all points should be from the original
	 * @return the OMGraphic with good points.
	 */
	protected OMGraphic getPushbackPoly(OMPoly omp, double dist) {
		double[] coords = omp.getLatLonArray();
		List<Geo> results = new LinkedList<Geo>();
		for (int i = 0; i < coords.length - 2; i += 2) {
			Geo g = new Geo(coords[i], coords[i + 1], false);
			if (!tooClose(g, dist)) {
				results.add(g);
			}
		}

		if (results.size() == 1) {
			results.add(new Geo(results.get(0)));
		}

		if (results.size() > 1) {
			return getOMPolyFromGeos(results);
		}

		return null;

	}

	/**
	 * Called when it's known that one of the legs between the corner is smaller
	 * than the buffer depth. Does some extra work to figure out what points
	 * should be added to the buffer.
	 * 
	 * @param g1 point 1
	 * @param g2 point 2, the corner
	 * @param g3 point 3
	 * @param ribbonSide which side of the ribbon should be calculated.
	 * @param dist the distance the buffer should be from the legs
	 * @param ret the OMGraphicList to add the resulting poly to.
	 */
	protected void addShortLegPolyForIntersection(Geo g1, Geo g2, Geo g3, int ribbonSide, double dist, OMAreaList ret) {

		/**
		 * We need to do some extra work here. Since one of the legs is shorter
		 * than 2*dist of the buffer, half of the buffer polygon won't reach the
		 * intersection point between the two legs. So, we need to recalculate
		 * the polys so the represent the entire legs, in order to find the
		 * intersection point.
		 * 
		 * Once we have that, we can go back to the half polygons, and test for
		 * that intersection point against each little segment. If any of those
		 * points are further away than buffer distance to the opposite poly,
		 * they should be included on the polygon added to the list. If a point
		 * is inside that distance, it should be disregarded. If there is only
		 * one point (i.e. the original intersection point), then a duplicate
		 * intersection point should be added, so the OMAreaList will handle it
		 * properly.
		 */
		List<Geo> results = new LinkedList<Geo>();
		RibbonIterator leg1 = new RibbonIterator(g1, g2, dist);
		OMPoly fullPoly1 = getPoly(leg1, ribbonSide);

		RibbonIterator leg2 = new RibbonIterator(g2, g3, dist);
		OMPoly fullPoly2 = getPoly(leg2, ribbonSide);

		if (fullPoly1 == null || fullPoly2 == null) {
			return;
		}

		// Intersection is the point on both polys that is buffer distance away
		// from corner
		Geo intersection = getPolyIntersection(fullPoly1, fullPoly2);

		if (intersection == null) {
			// GAAH! This shouldn't happen
			return;
		}

		leg1 = new RibbonIterator(g1, g2, dist);
		OMPoly halfPoly1 = getHalfPoly(leg1, ribbonSide, false);
		if (halfPoly1 != null) {
			GeoArray geoPoly2 = GeoArray.Double.createFromLatLonRadians(fullPoly2.getLatLonArray());

			double[] leg1Coords = halfPoly1.getLatLonArray();
			for (int i = 0; i < leg1Coords.length - 1; i += 2) {
				Geo pnt = new Geo(leg1Coords[i], leg1Coords[i + 1], false);

				if (!tooClose(pnt, dist)) {
					results.add(pnt);
				}
			}

			if (!tooClose(intersection, dist)) {
				results.add(intersection);
			}
		}

		leg2 = new RibbonIterator(g2, g3, dist);
		OMPoly halfPoly2 = getHalfPoly(leg2, ribbonSide, true);
		if (halfPoly2 != null) {
			GeoArray geoPoly1 = GeoArray.Double.createFromLatLonRadians(fullPoly1.getLatLonArray());

			double[] leg2Coords = halfPoly2.getLatLonArray();
			for (int i = 0; i < leg2Coords.length - 1; i += 2) {
				Geo pnt = new Geo(leg2Coords[i], leg2Coords[i + 1], false);

				if (!tooClose(pnt, dist)) {
					results.add(pnt);
				}
			}
		}

		if (results.size() == 1 && !tooClose(intersection, dist)) {
			results.add(intersection);
		}

		if (results.size() > 1) {
			ret.add(getOMPolyFromGeos(results));
		}
	}

	/**
	 * Just return the point where the two polygons cross.
	 * 
	 * @param poly1
	 * @param poly2
	 * @return null if no point found.
	 */
	protected Geo getPolyIntersection(OMPoly poly1, OMPoly poly2) {
		double[] p1Coords = poly1.getLatLonArray();
		double[] p2Coords = poly2.getLatLonArray();

		Geo a1, a2, b1, b2;
		Geo intersect = null;
		int index1 = 0, index2 = 0;
		for (; index2 + 3 < p2Coords.length; index2 += 2) {
			b1 = new Geo(p2Coords[index2], p2Coords[index2 + 1], false);
			b2 = new Geo(p2Coords[index2 + 2], p2Coords[index2 + 3], false);

			if (intersect == null) {
				for (; index1 + 3 < p1Coords.length; index1 += 2) {

					a1 = new Geo(p1Coords[index1], p1Coords[index1 + 1], false);
					a2 = new Geo(p1Coords[index1 + 2], p1Coords[index1 + 3], false);

					intersect = Intersection.segmentsIntersect(a1, a2, b1, b2);

					if (intersect != null) {
						return intersect;
					}
				}
			}
		}

		return intersect;
	}

	/**
	 * Converts Vector of Geos to an OMPoly with linetype great_circle. Assumes
	 * that the List has valid coordinates on it. Does not do a closeness check
	 * to the original poly, expected that's been done.
	 * 
	 * @param geos a set of coordinates
	 * @return OMPoly
	 */
	protected OMPoly getOMPolyFromGeos(List<Geo> geos) {
		double[] tmpCoords = new double[geos.size() * 2];
		int index = 0;
		for (Geo geo : geos) {
			tmpCoords[index++] = geo.getLatitudeRadians();
			tmpCoords[index++] = geo.getLongitudeRadians();
		}

		return new OMPoly(tmpCoords, OMPoly.RADIANS, OMGraphic.LINETYPE_GREATCIRCLE);
	}

	/**
	 * Called to handle BENDS_LEFT, concave corners. RuntimeException is thrown
	 * when one of the legs is much shorter than the other and the mid-point is
	 * still in the buffer of one of the legs. In that special case, the
	 * intersection point is not found.
	 * 
	 * @param poly1
	 * @param poly2
	 * @param ret
	 */
	protected void addPolyForIntersection(OMPoly poly1, OMPoly poly2, double dist, OMAreaList ret) {
		double[] p1Coords = poly1.getLatLonArray();
		double[] p2Coords = poly2.getLatLonArray();

		List<Geo> results = new LinkedList<Geo>();

		Geo a1, a2, b1, b2;
		Geo intersect = null;
		int index1 = 0, index2 = 0;
		for (; index2 + 3 < p2Coords.length; index2 += 2) {
			b1 = new Geo(p2Coords[index2], p2Coords[index2 + 1], false);
			b2 = new Geo(p2Coords[index2 + 2], p2Coords[index2 + 3], false);

			if (intersect == null) {
				for (; index1 + 3 < p1Coords.length; index1 += 2) {

					a1 = new Geo(p1Coords[index1], p1Coords[index1 + 1], false);
					a2 = new Geo(p1Coords[index1 + 2], p1Coords[index1 + 3], false);

					intersect = Intersection.segmentsIntersect(a1, a2, b1, b2);

					if (!tooClose(a1, dist)) {
						results.add(a1);
					}

					if (intersect != null) {
						if (!tooClose(intersect, dist)) {
							results.add(intersect);
						}
						break;
					}
				}
			}

			if (intersect != null && !tooClose(b2, dist)) {
				results.add(b2);
			}
		}

		if (results.size() > 1) {
			ret.add(getOMPolyFromGeos(results));
		}
	}

	/**
	 * Given a RibbonIterator created from two Geos, create a poly from half of
	 * that buffer path. Points are not checked for closeness to original poly.
	 * 
	 * @param rIterator RibbonIterator for one of the legs of corner
	 * @param side which RibbonIterator side
	 * @param first which half you want, true for first part
	 * @return OMPoly that represents half of the buffered path.
	 */
	protected OMPoly getHalfPoly(RibbonIterator rIterator, int side, boolean first) {
		List<Geo> results = new LinkedList<Geo>();
		for (Ribbon rib : rIterator) {
			Geo g = rib.get(side);
			results.add(g);
		}

		int numCoords = results.size();

		if (numCoords > 0) {

			int startingIndex = 0;
			int copyLength = numCoords / 2;

			if (numCoords % 2 == 0) {
				if (!first) {
					startingIndex = copyLength; // middle
				}
			} else {
				if (!first) {
					startingIndex = copyLength;
				}
				copyLength++;
			}

			List<Geo> newGeoCoords = new LinkedList<Geo>();
			for (int index = 0; index < copyLength; index++) {
				Geo g = results.get(startingIndex + index);
				newGeoCoords.add(g);
			}

			return getOMPolyFromGeos(newGeoCoords);
		}
		return null;
	}

	/**
	 * Given a RibbonIterator created from two Geos, create a poly from that
	 * buffer path.
	 * 
	 * @param rIterator RibbonIterator for one of the legs of corner
	 * @param side which RibbonIterator side
	 * @return OMPoly that represents buffered path between geos.
	 */
	protected OMPoly getPoly(RibbonIterator rIterator, int side) {
		List<Geo> bufferCoords = new LinkedList<Geo>();
		for (Ribbon rib : rIterator) {
			bufferCoords.add(rib.get(side));
		}

		if (bufferCoords.size() > 1) {
			return getOMPolyFromGeos(bufferCoords);
		}
		return null;
	}

	/**
	 * Given two polylines, with the end point of poly1 being the same distance
	 * from a point as the starting point of poly2, create an arc that connects
	 * them.
	 * 
	 * @param gc point
	 * @param poly1 polyline where the last end point is used
	 * @param poly2 polyline where the first end point is used.
	 * @return OMArc
	 */
	public OMGraphic getArc(Geo gc, OMPoly poly1, OMPoly poly2) {

		double[] poly1Coords = poly1.getLatLonArray();
		Geo pt1 = new Geo(poly1Coords[poly1Coords.length - 2], poly1Coords[poly1Coords.length - 1], false);
		double radAngle1 = gc.azimuth(pt1);

		double[] poly2Coords = poly2.getLatLonArray();
		Geo pt2 = new Geo(poly2Coords[0], poly2Coords[1], false);
		double radAngle2 = gc.azimuth(pt2);

		double dist = gc.distance(pt1);

		if (radAngle2 < radAngle1) {
			radAngle2 += MoreMath.TWO_PI_D;
		}

		if (logger.isLoggable(Level.FINE)) {
			logger.fine(
					new StringBuilder("Making arg starting at ").append(Length.DECIMAL_DEGREE.fromRadians(radAngle1))
							.append(", ").append(Length.DECIMAL_DEGREE.fromRadians(radAngle2 - radAngle1)).toString());
		}

		List<Geo> points = new LinkedList<Geo>();
		double inc = Length.DECIMAL_DEGREE.toRadians(2.0);
		double angle = radAngle1 + inc;

		while (angle < radAngle2 - inc) {
			Geo g = gc.offset(dist, angle);
			if (!tooClose(g, dist)) {
				points.add(g);
			}
			angle += inc;
		}

		return getOMPolyFromGeos(points);
		// return new OMArc(gc.getLatitude(), gc.getLongitude(), dist,
		// Length.RADIAN, 100, Length.DECIMAL_DEGREE.fromRadians(radAngle1),
		// Length.DECIMAL_DEGREE.fromRadians(radAngle2 - radAngle1));

	}
}
